﻿using System;
using System.IO;
using System.Text;

namespace Velocity4Net.Io
{

    /*
     * Licensed to the Apache Software Foundation (ASF) under one
     * or more contributor license agreements.  See the NOTICE file
     * distributed with this work for additional information
     * regarding copyright ownership.  The ASF licenses this file
     * to you under the Apache License, Version 2.0 (the
     * "License"); you may not use this file except in compliance
     * with the License.  You may obtain a copy of the License at
     *
     *   http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing,
     * software distributed under the License is distributed on an
     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
     * KIND, either express or implied.  See the License for the
     * specific language governing permissions and limitations
     * under the License.
     */




    /**
     * Implementation of a fast Writer. It was originally taken from JspWriter
     * and modified to have less synchronization going on.
     *
     * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
     * @author <a href="mailto:jon@latchkey.com">Jon S. Stevens</a>
     * @author Anil K. Vijendran
     * @version $Id: VelocityWriter.java 1855206 2019-03-11 11:09:53Z cbrisson $
     */
    public sealed class VelocityWriter : TextWriter, Filter
    {
        /**
         * constant indicating that the Writer is not buffering output
         */
        public const int NO_BUFFER = 0;

        /**
         * constant indicating that the Writer is buffered and is using the
         * implementation default buffer size
         */
        public const int DEFAULT_BUFFER = -1;

        /**
         * constant indicating that the Writer is buffered and is unbounded;
         * this is used in BodyContent
         */
        public const int UNBOUNDED_BUFFER = -2;

        private TextWriter writer = null;

        private int bufferSize;
        private bool autoFlush;

        private char []cb;
        private int nextChar;

        private static int defaultCharBufferSize = 8 * 1024;

        public override Encoding Encoding => throw new NotImplementedException();

        /**
         * Create a buffered character-output stream that uses a default-sized
         * output buffer.
         *
         * @param  writer  Writer to wrap around
         */
        public VelocityWriter(TextWriter writer):this(writer, defaultCharBufferSize, true)
        {
        }

        /**
         * private constructor.
         */
        private VelocityWriter(int bufferSize, bool autoFlush)
        {
            this.bufferSize = bufferSize;
            this.autoFlush = autoFlush;
        }

        /**
         * This method returns the size of the buffer used by the JspWriter.
         *
         * @return the size of the buffer in bytes, or 0 is unbuffered.
         */
        public int getBufferSize() { return bufferSize; }

        /**
         * This method indicates whether the JspWriter is autoFlushing.
         *
         * @return if this JspWriter is auto flushing or throwing IOExceptions on
         *         buffer overflow conditions
         */
        public bool isAutoFlush() { return autoFlush; }

        /**
         * Create a new buffered character-output stream that uses an output
         * buffer of the given size.
         *
         * @param  writer  Writer to wrap around
         * @param  sz   	Output-buffer size, a positive integer
         * @param autoFlush
         *
         * @exception  IllegalArgumentException  If sz is &lt;= 0
         */
        public VelocityWriter(TextWriter writer, int sz, bool autoFlush):this(sz, autoFlush)
        {
            if (sz < 0)
                throw new ArgumentException("Buffer size <= 0");
            this.writer = writer;
            cb = sz == 0 ? null : new char[sz];
            nextChar = 0;
        }

        /**
         * Flush the output buffer to the underlying character stream, without
         * flushing the stream itself.  This method is non-private only so that it
         * may be invoked by PrintStream.
         */
        private void flushBuffer()
        {
            if (bufferSize == 0)
                return;
            if (nextChar == 0)
                return;
            writer.Write(cb, 0, nextChar);
            nextChar = 0;
        }

        /**
         * Discard the output buffer.
         */
        public void clear()
        {
            nextChar = 0;
        }

        private void bufferOverflow()
        {
            throw new IOException("overflow");
        }

        /**
         * Flush the stream.
         * @throws IOException
         *
         */
        public void flush()
        {
            flushBuffer();
            if (writer != null)
            {
                writer.Flush();
            }
        }

        /**
         * Close the stream.
         * @throws IOException
         *
         */
        public void close()
        {
            if (writer == null)
                return;
            flush();
        }

        /**
         * @return the number of bytes unused in the buffer
         */
        public int getRemaining()
        {
            return bufferSize - nextChar;
        }

        /**
         * Write a single character.
         * @param c
         * @throws IOException
         *
         */
        public void write(int c)
        {
            if (bufferSize == 0)
            {
                writer.Write(c);
            }
            else
            {
                if (nextChar >= bufferSize)
                    if (autoFlush)
                        flushBuffer();
                    else
                        bufferOverflow();
                cb[nextChar++] = (char)c;
            }
        }

        /**
         * Our own little min method, to avoid loading
         * <code>java.lang.Math</code> if we've run out of file
         * descriptors and we're trying to print a stack trace.
         */
        private int min(int a, int b)
        {
            return (a < b ? a : b);
        }

        /**
         * Write a portion of an array of characters.
         *
         * <p> Ordinarily this method stores characters from the given array into
         * this stream's buffer, flushing the buffer to the underlying stream as
         * needed.  If the requested length is at least as large as the buffer,
         * however, then this method will flush the buffer and write the characters
         * directly to the underlying stream.  Thus redundant
         * <code>DiscardableBufferedWriter</code>s will not copy data unnecessarily.
         *
         * @param  cbuf  A character array
         * @param  off   Offset from which to start reading characters
         * @param  len   Number of characters to write
         * @throws IOException
         *
         */
        public void write(char []cbuf, int off, int len)
        {
            if (bufferSize == 0)
            {
                writer.Write(cbuf, off, len);
                return;
            }

            if (len == 0)
            {
                return;
            }

            if (len >= bufferSize)
            {
                /* If the request length exceeds the size of the output buffer,
                flush the buffer and then write the data directly.  In this
                way buffered streams will cascade harmlessly. */
                if (autoFlush)
                    flushBuffer();
                else
                    bufferOverflow();
                writer.Write(cbuf, off, len);
                return;
            }

            int b = off, t = off + len;
            while (b < t)
            {
                int d = min(bufferSize - nextChar, t - b);
                Array.Copy(cbuf, b, cb, nextChar, d);
                b += d;
                nextChar += d;
                if (nextChar >= bufferSize)
                    if (autoFlush)
                        flushBuffer();
                    else
                        bufferOverflow();
            }
        }

        /**
         * Write an array of characters.  This method cannot be inherited from the
         * Writer class because it must suppress I/O exceptions.
         * @param buf
         * @throws IOException
         */
        public void write(char []buf)
        {
            write(buf, 0, buf.Length);
        }

        /**
         * Write a portion of a String.
         *
         * @param  s     String to be written
         * @param  off   Offset from which to start reading characters
         * @param  len   Number of characters to be written
         * @throws IOException
         *
         */
        public void write(String s, int off, int len)
        {
            if (bufferSize == 0)
            {
                writer.Write(s, off, len);
                return;
            }
            int b = off, t = off + len;
            while (b < t)
            {
                int d = min(bufferSize - nextChar, t - b);
                //s.getChars(b, b + d, cb, nextChar);
                cb = s.ToCharArray(b, b + d);
                b += d;
                nextChar += d;
                if (nextChar >= bufferSize)
                    if (autoFlush)
                        flushBuffer();
                    else
                        bufferOverflow();
            }
        }

        /**
         * Write a string.  This method cannot be inherited from the Writer class
         * because it must suppress I/O exceptions.
         * @param s
         * @throws IOException
         */
        public void write(String s)
        {
            if (s != null)
            {
                write(s, 0, s.Length);
            }
        }

        /**
         * resets this class so that it can be reused
         * @param writer
         *
         */
        public void recycle(TextWriter writer)
        {
            this.writer = writer;
            clear();
        }

        /**
         * Send the content of a reference, e.g.; $foo, to the writer.
         * The default implementation is to call the wrapped Writer's
         * write(String) method.
         */
        public void writeReference(String _ref)
        {
            write(_ref);
        }

    }
}